Skip to content

feat: headless Slack workflow runner via ACP (#48)#49

Open
m2ux wants to merge 18 commits intomainfrom
feat/headless-slack-runner
Open

feat: headless Slack workflow runner via ACP (#48)#49
m2ux wants to merge 18 commits intomainfrom
feat/headless-slack-runner

Conversation

@m2ux
Copy link
Copy Markdown
Owner

@m2ux m2ux commented Mar 5, 2026

Summary

Adds a headless Slack workflow runner enabling AI-driven workflow execution triggered from Slack without a GUI. 9 modules (~1,410 LOC) with SQLite persistence, structured logging, graceful shutdown, and orphan worktree recovery.

🎫 Ticket 📐 Engineering 🧪 Test Plan


Motivation

The runner existed as a PoC that kept all session state in an in-memory Map, logged to the console with no file output, and left agent processes and worktrees behind on shutdown or crash. These gaps prevented it from being used for real workflow execution — a runner restart mid-workflow would lose all context, overnight crashes left no diagnostic trail, and orphaned worktrees accumulated on disk.

This PR hardens the runner for merge by adding SQLite state persistence (via node:sqlite, zero new dependencies), structured pino logging with daily file rotation, and full resource cleanup on both graceful shutdown and crash recovery.


Changes

  • Session persistence (session-store.ts) — SQLite-backed session metadata storage using Node.js built-in node:sqlite; sessions survive runner restarts
  • Session orchestration (session-manager.ts) — Lifecycle management for create → run → checkpoint relay → shutdown, with persistence at each state transition
  • ACP client (acp-client.ts) — Cursor ACP agent process lifecycle and stdin/stdout protocol handling
  • Slack integration (slack-bot.ts) — Slack Bolt app setup, slash-command routing, thread messaging via Socket Mode
  • Checkpoint bridge (checkpoint-bridge.ts) — Bridges ACP checkpoint events to Slack interactive messages for human-in-the-loop approval
  • Worktree isolation (worktree-manager.ts) — Git worktree creation/teardown per session; orphan sweep on startup removes stale wf-runner-* worktrees
  • Configuration (config.ts) — Zod-validated config from environment variables
  • Structured logging (logger.ts) — pino + pino-roll for rotating structured JSON log files with session context
  • Entrypoint (index.ts) — Bootstrap, signal handling (SIGINT/SIGTERM), graceful shutdown orchestration
  • Slack app manifest (docs/slack-app-manifest.yml) — Declarative Slack app configuration for the runner
  • Setup guide (docs/runner-setup.md) — Installation and configuration instructions
  • Tests — 5 test files, 43 unit tests (~920 LOC) covering session-manager, session-store, acp-client, checkpoint-bridge, and worktree-manager

Module Overview

Module LOC Responsibility
acp-client.ts 332 Cursor ACP agent process lifecycle, stdin/stdout protocol handling
session-manager.ts 362 Session orchestration — create, run, checkpoint relay, shutdown
worktree-manager.ts 173 Git worktree creation/teardown, orphan sweep on startup
slack-bot.ts 161 Slack Bolt app setup, slash-command routing, thread messaging
checkpoint-bridge.ts 141 Bridges ACP checkpoint events to Slack interactive messages
session-store.ts 102 SQLite persistence for session metadata and state transitions
config.ts 73 Zod-validated config from environment variables
index.ts 49 Entrypoint — bootstrap, signal handling, graceful shutdown
logger.ts 20 Structured logging via pino + pino-roll

Key Capabilities

  • Slack-triggered execution — users invoke workflows via Slack slash commands; progress and checkpoints surface in threads
  • Git worktree isolation — each workflow session gets its own worktree (wf-runner-<id>), preventing interference between concurrent runs
  • SQLite state persistence — session metadata survives process restarts via node:sqlite
  • Structured logging — pino with pino-roll for rotating log files; all events include session context
  • Graceful shutdown — SIGINT/SIGTERM triggers shutdownAll() which cleans up active sessions, closes the DB, and stops the Slack listener
  • Orphan worktree sweep — on startup, removes leftover wf-runner-* worktrees from prior unclean exits
  • Input validation — Zod schema validation on config; regex validation on slash command arguments

📌 Submission Checklist

  • Implementation complete (9 modules, ~1,410 LOC)
  • Unit tests passing (43 tests across 5 files, ~920 LOC)
  • TypeScript typecheck passing
  • Strategic review passed
  • All commits pushed to remote
  • Slack app manifest included (docs/slack-app-manifest.yml)
  • Setup guide included (docs/runner-setup.md)

🗹 TODO before merging

  • Ready for review

Introduces a runner module that enables headless workflow execution
driven by Slack, using Cursor's Agent Client Protocol (ACP) for the
agent runtime. Each workflow run gets an isolated git worktree and
its own agent process, allowing parallel execution.

Components:
- ACP client: JSON-RPC 2.0 over stdio to `agent acp`
- Slack bot: Bolt SDK with Socket Mode, /workflow slash command
- Checkpoint bridge: cursor/ask_question <-> Slack interactive messages
- Session manager: lifecycle tracking, status streaming to threads
- Worktree manager: git worktree creation/cleanup, MCP + permission config

Made-with: Cursor
@m2ux m2ux self-assigned this Mar 5, 2026
m2ux added 11 commits March 5, 2026 11:26
Cursor CLI emits diagnostic output on stderr that was being treated as
an error, causing false-positive noise. Change the stderr handler in
AcpClient to emit a 'stderr' event instead of 'error', and wire the
new event to console.error in SessionManager for visibility.

Made-with: Cursor
Iterates active sessions and cleans up worktrees/agents before
stopping the Slack app on SIGINT/SIGTERM.

Made-with: Cursor
- New logger module with daily rotation and 14-file retention
- Replace all console.* calls with structured pino logging
- Wire stderr events to logger.debug
- Add LOG_LEVEL env var support to config schema

Made-with: Cursor
- New SessionStore class with sessions table (create/load/update/close)
- SessionManager accepts optional store, persists session lifecycle
- Stale sessions from previous runs are logged and marked as errored
- Add DB_PATH env var to config schema (default: data/runner.db)

Made-with: Cursor
- Rename directory prefix from run- to wf-runner- for disambiguation
- Rename branch prefix from runner/ to wf-runner/
- Add sweepOrphaned() method that removes stale worktrees on startup
- Update tests for new prefix

Made-with: Cursor
H1: Log warnings when auto-approving permission requests for tools not
    in the known APPROVED_TOOL_TYPES set (session-manager.ts).

H2: Add configurable timeout (default 60s) to JSON-RPC send() method.
    Pending promises are rejected with a descriptive error on timeout.
    Long-running prompt/followUp calls opt out with timeout=0.

H3: Validate submodule paths against path traversal (worktree-manager.ts)
    and validate slash command arguments against allowlisted patterns
    (slack-bot.ts).
Made-with: Cursor
- Upgrade @types/node from ^20 to ^22 for node:sqlite type support
- Fix type cast in session-store.ts for updated stmt.all() return type
- Fix mcp-server test resource index from "00" to "01"

Made-with: Cursor
S1: Revert .engineering submodule pointer to match main — the pointer
was updated during planning and is unrelated to the runner feature.

S2: Keep mcp-server.test.ts fix (index '00' → '01') — resource '00'
never existed, so this fixes a genuinely broken test on main.

O1: Remove dead followUp() method from AcpClient — no callers in the
codebase, functionally identical to prompt().

Made-with: Cursor
@m2ux m2ux marked this pull request as ready for review March 5, 2026 14:47
m2ux added 2 commits March 5, 2026 14:48
Covers Slack app creation, environment configuration, starting the
runner, executing workflows via slash commands, monitoring, and
troubleshooting.

Made-with: Cursor
@m2ux m2ux changed the title feat: headless Slack workflow runner via Cursor ACP (#48) feat: headless Slack workflow runner via ACP (#48) Mar 6, 2026
m2ux added 4 commits March 11, 2026 13:59
Provides the Slack app configuration (slash commands, bot scopes,
Socket Mode) needed to set up the runner's Slack integration.

Made-with: Cursor
Change checkpoint.blocking from z.literal(true) to z.boolean() in the
Zod schema and remove "const": true from the JSON schema. Non-blocking
checkpoints are now valid. Fixes 22 test failures caused by work-package
activities that use blocking: false.

Made-with: Cursor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant